[id].vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <template>
  2. <div class="admin--sales-form">
  3. <div v-if="isLoading" class="admin--loading">
  4. 데이터를 불러오는 중...
  5. </div>
  6. <form v-else @submit.prevent="handleSubmit" class="admin--form">
  7. <!-- 전시장 -->
  8. <div class="admin--form-group">
  9. <label class="admin--form-label">전시장 <span class="admin--required">*</span></label>
  10. <select v-model="formData.showroom_id" class="admin--form-select" required>
  11. <option value="">전시장을 선택하세요</option>
  12. <option v-for="showroom in showrooms" :key="showroom.id" :value="showroom.id">
  13. {{ showroom.name }}
  14. </option>
  15. </select>
  16. </div>
  17. <!-- 영업팀 -->
  18. <div class="admin--form-group">
  19. <label class="admin--form-label">영업팀 <span class="admin--required">*</span></label>
  20. <select v-model="formData.team_id" class="admin--form-select" required>
  21. <option value="">영업팀을 선택하세요</option>
  22. <option v-for="team in teams" :key="team.id" :value="team.id">
  23. {{ team.name }}
  24. </option>
  25. </select>
  26. </div>
  27. <!-- 이름 -->
  28. <div class="admin--form-group">
  29. <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
  30. <input
  31. v-model="formData.name"
  32. type="text"
  33. class="admin--form-input"
  34. placeholder="이름을 입력하세요"
  35. required
  36. >
  37. </div>
  38. <!-- 직책 -->
  39. <div class="admin--form-group">
  40. <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
  41. <select v-model.number="formData.position" class="admin--form-select" required>
  42. <option value="">직책을 선택하세요</option>
  43. <option :value="10">팀장</option>
  44. <option :value="15">마스터</option>
  45. <option :value="20">차장</option>
  46. <option :value="30">과장</option>
  47. <option :value="40">대리</option>
  48. <option :value="60">사원</option>
  49. </select>
  50. </div>
  51. <!-- 대표번호 -->
  52. <div class="admin--form-group">
  53. <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
  54. <input
  55. v-model="formData.main_phone"
  56. type="tel"
  57. class="admin--form-input"
  58. placeholder="02-1234-5678"
  59. required
  60. >
  61. </div>
  62. <!-- 직통번호 -->
  63. <div class="admin--form-group">
  64. <label class="admin--form-label">직통번호</label>
  65. <input
  66. v-model="formData.direct_phone"
  67. type="tel"
  68. class="admin--form-input"
  69. placeholder="02-1234-5679"
  70. >
  71. </div>
  72. <!-- 핸드폰 -->
  73. <div class="admin--form-group">
  74. <label class="admin--form-label">핸드폰</label>
  75. <input
  76. v-model="formData.mobile"
  77. type="tel"
  78. class="admin--form-input"
  79. placeholder="010-1234-5678"
  80. >
  81. </div>
  82. <!-- 이메일 -->
  83. <div class="admin--form-group">
  84. <label class="admin--form-label">이메일</label>
  85. <input
  86. v-model="formData.email"
  87. type="email"
  88. class="admin--form-input"
  89. placeholder="email@example.com"
  90. >
  91. </div>
  92. <!-- 사진 -->
  93. <div class="admin--form-group">
  94. <label class="admin--form-label">사진</label>
  95. <input
  96. type="file"
  97. accept="image/*"
  98. class="admin--form-file"
  99. @change="handlePhotoUpload"
  100. >
  101. <div v-if="photoPreview || formData.photo_url" class="admin--image-preview">
  102. <img :src="photoPreview || getImageUrl(formData.photo_url)" alt="미리보기">
  103. <button type="button" class="admin--btn-remove-image" @click="removePhoto">
  104. 삭제
  105. </button>
  106. </div>
  107. </div>
  108. <!-- SACT -->
  109. <div class="admin--form-group">
  110. <label class="admin--form-label">SACT</label>
  111. <div class="admin--radio-group">
  112. <label class="admin--radio-label">
  113. <input v-model="formData.is_sact" type="radio" :value="true" name="is_sact">
  114. <span>예</span>
  115. </label>
  116. <label class="admin--radio-label">
  117. <input v-model="formData.is_sact" type="radio" :value="false" name="is_sact">
  118. <span>아니오</span>
  119. </label>
  120. </div>
  121. </div>
  122. <!-- TOP30 -->
  123. <div class="admin--form-group">
  124. <label class="admin--form-label">TOP30</label>
  125. <div class="admin--radio-group">
  126. <label class="admin--radio-label">
  127. <input v-model="formData.is_top30" type="radio" :value="true" name="is_top30">
  128. <span>예</span>
  129. </label>
  130. <label class="admin--radio-label">
  131. <input v-model="formData.is_top30" type="radio" :value="false" name="is_top30">
  132. <span>아니오</span>
  133. </label>
  134. </div>
  135. </div>
  136. <!-- 노출순서 -->
  137. <div class="admin--form-group">
  138. <label class="admin--form-label">노출순서</label>
  139. <input
  140. v-model.number="formData.display_order"
  141. type="number"
  142. class="admin--form-input"
  143. placeholder="숫자만 입력"
  144. min="0"
  145. >
  146. <p class="admin--form-help">숫자가 작을수록 먼저 노출됩니다.</p>
  147. </div>
  148. <!-- 버튼 영역 -->
  149. <div class="admin--form-actions">
  150. <button
  151. type="submit"
  152. class="admin--btn admin--btn-primary"
  153. :disabled="isSaving"
  154. >
  155. {{ isSaving ? '저장 중...' : '확인' }}
  156. </button>
  157. <button
  158. type="button"
  159. class="admin--btn admin--btn-secondary"
  160. @click="goToList"
  161. >
  162. 목록
  163. </button>
  164. </div>
  165. <!-- 성공/에러 메시지 -->
  166. <div v-if="successMessage" class="admin--alert admin--alert-success">
  167. {{ successMessage }}
  168. </div>
  169. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  170. {{ errorMessage }}
  171. </div>
  172. </form>
  173. </div>
  174. </template>
  175. <script setup>
  176. import { ref, onMounted } from 'vue'
  177. import { useRoute, useRouter } from 'vue-router'
  178. definePageMeta({
  179. layout: 'admin',
  180. middleware: ['auth']
  181. })
  182. const route = useRoute()
  183. const router = useRouter()
  184. const { get, put, upload } = useApi()
  185. const { getImageUrl } = useImage()
  186. const isLoading = ref(true)
  187. const isSaving = ref(false)
  188. const successMessage = ref('')
  189. const errorMessage = ref('')
  190. const photoPreview = ref(null)
  191. const photoFile = ref(null)
  192. const showrooms = ref([])
  193. // 영업팀 수동 리스트 (0번째는 마스터팀)
  194. const teams = ref([
  195. { id: 0, name: '마스터팀' },
  196. { id: 1, name: '1팀' },
  197. { id: 2, name: '2팀' },
  198. { id: 3, name: '3팀' },
  199. { id: 4, name: '4팀' },
  200. { id: 5, name: '5팀' },
  201. { id: 6, name: '6팀' },
  202. { id: 7, name: '7팀' },
  203. { id: 8, name: '8팀' },
  204. { id: 9, name: '9팀' },
  205. { id: 10, name: '10팀' }
  206. ])
  207. const formData = ref({
  208. showroom_id: '',
  209. team_id: '',
  210. name: '',
  211. position: '',
  212. main_phone: '',
  213. direct_phone: '',
  214. mobile: '',
  215. email: '',
  216. photo_url: '',
  217. is_sact: false,
  218. is_top30: false,
  219. display_order: 0
  220. })
  221. // 필터 데이터 로드
  222. const loadFilters = async () => {
  223. // 전시장 리스트 (지점 목록)
  224. const { data: branchData, error: branchError } = await get('/branch/list', { per_page: 1000 })
  225. console.log('[SalesEdit] 전시장(지점) API 응답:', { data: branchData, error: branchError })
  226. if (branchData?.success && branchData?.data) {
  227. showrooms.value = branchData.data.items || []
  228. console.log('[SalesEdit] 전시장(지점) 로드 성공')
  229. }
  230. }
  231. // 데이터 로드
  232. const loadSales = async () => {
  233. isLoading.value = true
  234. const id = route.params.id
  235. const { data, error } = await get(`/staff/sales/${id}`)
  236. console.log('[SalesEdit] 데이터 로드:', { data, error })
  237. if (data?.success && data?.data) {
  238. const sales = data.data
  239. formData.value = {
  240. showroom_id: sales.showroom_id || '',
  241. team_id: sales.team_id || '',
  242. name: sales.name || '',
  243. position: sales.position || '',
  244. main_phone: sales.main_phone || '',
  245. direct_phone: sales.direct_phone || '',
  246. mobile: sales.mobile || '',
  247. email: sales.email || '',
  248. photo_url: sales.photo_url || '',
  249. is_sact: sales.is_sact || false,
  250. is_top30: sales.is_top30 || false,
  251. display_order: sales.display_order || 0
  252. }
  253. console.log('[SalesEdit] 로드 성공')
  254. }
  255. isLoading.value = false
  256. }
  257. // 사진 업로드
  258. const handlePhotoUpload = (event) => {
  259. const file = event.target.files[0]
  260. if (!file) return
  261. if (!file.type.startsWith('image/')) {
  262. alert('이미지 파일만 업로드 가능합니다.')
  263. return
  264. }
  265. photoFile.value = file
  266. const reader = new FileReader()
  267. reader.onload = (e) => {
  268. photoPreview.value = e.target.result
  269. }
  270. reader.readAsDataURL(file)
  271. }
  272. // 사진 삭제
  273. const removePhoto = () => {
  274. photoPreview.value = null
  275. photoFile.value = null
  276. formData.value.photo_url = ''
  277. }
  278. // 폼 제출
  279. const handleSubmit = async () => {
  280. successMessage.value = ''
  281. errorMessage.value = ''
  282. if (!formData.value.showroom_id || !formData.value.team_id) {
  283. errorMessage.value = '전시장, 영업팀을 선택하세요.'
  284. return
  285. }
  286. if (!formData.value.name) {
  287. errorMessage.value = '이름을 입력하세요.'
  288. return
  289. }
  290. isSaving.value = true
  291. try {
  292. let photoUrl = formData.value.photo_url
  293. // 새 사진 업로드
  294. if (photoFile.value) {
  295. const formDataImage = new FormData()
  296. formDataImage.append('file', photoFile.value)
  297. const { data: uploadData, error: uploadError } = await upload('/upload/staff-image', formDataImage)
  298. if (uploadError) {
  299. errorMessage.value = '사진 업로드에 실패했습니다.'
  300. isSaving.value = false
  301. return
  302. }
  303. photoUrl = uploadData.data?.url || uploadData.url
  304. }
  305. const submitData = {
  306. ...formData.value,
  307. photo_url: photoUrl
  308. }
  309. const id = route.params.id
  310. const { data, error } = await put(`/staff/sales/${id}`, submitData)
  311. if (error) {
  312. errorMessage.value = error.message || '수정에 실패했습니다.'
  313. } else {
  314. successMessage.value = '영업사원 정보가 수정되었습니다.'
  315. setTimeout(() => {
  316. router.push('/admin/staff/sales')
  317. }, 1000)
  318. }
  319. } catch (error) {
  320. errorMessage.value = '서버 오류가 발생했습니다.'
  321. console.error('Save error:', error)
  322. } finally {
  323. isSaving.value = false
  324. }
  325. }
  326. const goToList = () => {
  327. router.push('/admin/staff/sales')
  328. }
  329. onMounted(async () => {
  330. await loadFilters()
  331. await loadSales()
  332. })
  333. </script>